﻿#region [ License information          ]
/* ************************************************************
 *
 * Copyright (c) Bjørn Bouet Smith, 2010, 2011
 *
 * This source code is subject to terms and conditions of 
 * Microsoft Public License (Ms-PL).
 * 
 * A copy of the license can be found in the license.txt
 * file at the root of this distribution. If you can not 
 * locate the License, please send an email to bjornsmith@gmail.com
 * 
 * By using this source code in any fashion, you are 
 * agreeing to be bound by the terms of the Microsoft 
 * Permissive License.
 *
 * You must not remove this notice, or any other, from this
 * software.
 *
 * ************************************************************/
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Smith.AMC.Configuration;
using Smith.AMC.Operations;
using Wintellect.Threading.AsyncProgModel;

namespace Smith.AMC.Network
{
    /// <summary>
    /// Monitors memcached server nodes to check if they are still alive, by doing a simple get request against them
    /// If the response is okay, the server node's IsAlive property is set to true; otherwise its set to false.
    /// Will periodically check all server nodes with a high ErrorCount
    /// </summary>
    public class ServerNodeMonitor
    {
        /// <summary>
        /// List of memcached servers
        /// </summary>
        private readonly List<IMemcachedServerNode> servers;

        /// <summary>
        /// The number of times a ServerNode is allowed to have an error reported against it before it will be checked.
        /// This is to prevent the monitor from monitoring servers in vain. I.e. servers that work just fine require no monitoring.
        /// Its only when they start behaving weird that we should start monitoring.
        /// </summary>
        private readonly int failedCountThresHold;

        /// <summary>
        /// The interval in seconds in between the monitor will check each server node.
        /// </summary>
        private readonly int serverNodeHealthCheckIntervalInSeconds;

        /// <summary>
        /// The ResetEvent that is controlling the monitor thread.
        /// </summary>
        private readonly ManualResetEvent quitEvent = new ManualResetEvent(false);

        /// <summary>
        /// The thread that will run the monitoring
        /// </summary>
        private Thread runThread;

        /// <summary>
        /// Initializes a new instance of the <see cref="ServerNodeMonitor"/> class.
        /// </summary>
        /// <param name="servers">The list of servers to monitor.</param>
        /// <param name="configuration">The configuration for the monitor.</param>
        public ServerNodeMonitor(List<IMemcachedServerNode> servers, ServerNodeMonitorConfiguration configuration)
        {
            this.servers = servers;
            failedCountThresHold = configuration.FailedThreshold;
            serverNodeHealthCheckIntervalInSeconds = (int)configuration.CheckInterval.TotalSeconds;
        }

        /// <summary>
        /// Starts the server node monitor - which will cause the monitor to start monitoring the servers that participate in the cluster.
        /// </summary>
        public void Start()
        {
            if (runThread != null)
            {
                throw new InvalidOperationException("Monitor thread already active");
            }

            runThread = new Thread(RunMonitoring);
            runThread.Start();
        }

        /// <summary>
        /// Blocking call to stop the server node monitor.
        /// </summary>
        public void Stop()
        {
            ManualResetEvent evt = new ManualResetEvent(false);
            BeginStop(
                ar =>
                {
                    EndStop(ar);
                    ManualResetEvent evtInner = (ManualResetEvent)ar.AsyncState;
                    evtInner.Set();
                },
            evt);

            evt.WaitOne();
        }

        /// <summary>
        /// Begins an asynchronous operation to tell the monitor to stop.
        /// </summary>
        /// <param name="callback">The <see cref="System.AsyncCallback"/> delegate that is executed when the BeginStop operation completes</param>
        /// <param name="state">An object that contains any additional user-defined data.</param>
        /// <returns>
        /// An <see cref="System.IAsyncResult"/> that represents the asynchronous call.
        /// </returns>
        public IAsyncResult BeginStop(AsyncCallback callback, object state)
        {
            quitEvent.Set();
            AsyncResult asyncResult = new AsyncResult(callback, state, this);
            new Thread(() =>
            {
                runThread.Join();
                asyncResult.SetAsCompleted(null, false);
            }).Start();

            return asyncResult;
        }

        /// <summary>
        /// Handles the end of the asynchronous add operation
        /// </summary>
        /// <param name="asyncResult">An <see cref="System.IAsyncResult"/> that represents the asynchronous call.</param>
        public void EndStop(IAsyncResult asyncResult)
        {
            runThread = null;
        }

        /// <summary>
        /// The the method that the thread will be running.
        /// Will run until the quitEvent of the servernodemonitor class has been set.
        /// </summary>
        private void RunMonitoring()
        {
            do
            {
                // Grab the list of server nodes where the error count is high or they have been marked as dead
                List<IMemcachedServerNode> memcachedServerNodes = servers.Where(s => s.FailedCount > failedCountThresHold || s.IsDead).ToList();

                foreach (IMemcachedServerNode serverNode in memcachedServerNodes)
                {
                    GetOperation operation = new GetOperation
                    {
                        Key =
                        "health"
                    };
                    ManualResetEvent evt = new ManualResetEvent(false);

                    IMemcachedServerNode node = serverNode;
                    serverNode.HealthCheckSocket.BeginWrite<int>(
                    operation,
                    ar =>
                    {
                        try
                        {
                            node.HealthCheckSocket.EndWrite<int>(ar);
                            node.IsDead = false;

                            // TODO: Possibly only subtract the value we previously read when we decided to start checking the server.
                            node.FailedCount = 0;
                        }
                        catch (Exception e)
                        {
                            // TODO: Log this exception and mark the node as dead.
                            Console.WriteLine(e);
                            node.IsDead = true;
                        }
                        finally
                        {
                            evt.Set();
                        }
                    },
                    evt);

                    evt.WaitOne();

                    // Wait for one ms to make owner of the monitor capable of telling this instance to quit.
                    if (quitEvent.WaitOne(1))
                    {
                        break;
                    }
                }
            }
            while (!quitEvent.WaitOne(serverNodeHealthCheckIntervalInSeconds * 1000));
        }
    }
}
